package com.uxsino.simo.collector.connections;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.Map;

import com.uxsino.commons.utils.StringUtils;
import org.apache.commons.net.telnet.TelnetClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.uxsino.simo.connections.AbstractConnection;
import com.uxsino.simo.connections.exception.SimoConnectionException;
import com.uxsino.simo.connections.target.TelnetTarget;

public class TelnetConnection extends AbstractConnection<TelnetTarget> {

    private static Logger logger = LoggerFactory.getLogger(TelnetConnection.class);

    private final static String CRString = "\r\n";

    private final static String charsetDetectStr = "locale charmap";

    private final static String UTF_8 = "UTF-8";
    private final static String UTF8 = "UTF8";

    private final static int DEFAULT_TIME_OUT = 900000;

    private final static String CHANNEL_PTY_TYPE = "dumb";

    private boolean isSpecialCharset = false;

    private String charsetName = "";

    private TelnetClient telnet = null;

    private InputStream fromTerminal = null;

    private PrintStream toHost = null;

    public TelnetConnection() {
        telnet = new TelnetClient(CHANNEL_PTY_TYPE);
    }

    // update connected to be true if successful
    @Override
    public int connect(TelnetTarget target) {
        super.connect(target);
        this.target = target;
        connected = false;
        state = 1;
        try {
            telnet.setDefaultTimeout(DEFAULT_TIME_OUT);
            telnet.connect(target.host, target.port);
            fromTerminal = telnet.getInputStream();
            toHost = new PrintStream(telnet.getOutputStream());

            tillNextPrompt(target.getLoginPrompt(), false, null);
            writeToHost(target.getUsername());
            tillNextPrompt(target.getPasswordPrompt(), false, null);
            writeToHost(target.getPassword());

            String[] prompts = { target.getFailPrompt(), target.getPrompt() };
            // 1 below is the index of target.getPrompt() in the array prompts
            connected = (checkResponsePrompt(prompts) == 1);

            if (!connected) {
                logger.error("telnet " + target.host + ":" + target.port + ": " + " incorrect username/password pair.");
                close();
                state = 0;
            }
            writeToHost(charsetDetectStr);
            charsetName = tillNextPrompt(target.getPrompt(), true, charsetDetectStr).trim().toUpperCase();
            if (!charsetName.equals("\"\"")&&!charsetName.equals(UTF_8)&&!charsetName.equals(UTF8)) {
                isSpecialCharset = true;
                logger.info("charname {}",charsetName);
            }

        } catch (Exception e) {
            logger.error("failed to connect on telnet: {}", "", e);
            close();
            state = 0;
        }
        return state;
    }

    @Override
    public Object execCmd(Object cmdPattern) throws SimoConnectionException {
        if (!isConnected()) {
            logger.error("telnet: can not exec command for it is not connected");
            throw new SimoConnectionException("no connection valid.");
        }

        writeToHost((String) cmdPattern);
        String result = null;
        if (((String) cmdPattern).startsWith("cd ")) {
            result = tillNextPrompt(target.getPrompt(), false, (String) cmdPattern);
        } else {
            result = tillNextPrompt(target.getPrompt(), true, (String) cmdPattern);
        }
        return result;
    }

    @Override
    public Object buildCmd(String cmdPattern, Map<String, String> args) {
        return cmdPattern;
    }

    // update connected to be false
    // various implementations of the AbstractConnection are not consistent
    @Override
    public int close() {
        try {
            if (telnet != null) {
                telnet.disconnect();
            }
        } catch (Exception e) {
            logger.error("telnet disconnect failure: {}", "", e);
        } finally {
            try {
                if (fromTerminal != null) {
                    fromTerminal.close();
                }
            } catch (IOException e) {
                logger.error("telnet in stream closing I/O failure: {}", "", e);
            }
        }
        connected = false;
        super.close();
        return 0;
    }

    private void writeToHost(String msg) {
        toHost.println(msg);
        toHost.flush();
    }

    /**
     * Reads the fromTerminal stream until one of the prompts in the parameter
     * occurs then return the prompt that did occur This method does not care if
     * carriage return occurs since only the prompt is kept
     **/

    private int checkResponsePrompt(String[] prompts) {
        int result = -1;
        try {
            // collect the last char in the prompts into a string
            StringBuilder endsb = new StringBuilder();
            for (int i = 0; i < prompts.length; i++) {
                endsb.append(prompts[i].charAt(prompts[i].length() - 1));
            }
            String ends = endsb.toString();

            StringBuilder sb = new StringBuilder();
            char ch;
            while (result == -1) {
                ch = (char) fromTerminal.read();
                sb.append(ch);
                // the last char of the prompts might have duplicates
                // the loop runs through all the duplicates
                int tail = ends.length();
                while ((tail > 0) && (ends.lastIndexOf(ch, tail - 1) != -1)) {
                    tail = ends.lastIndexOf(ch, tail - 1);
                    if (sb.toString().endsWith(prompts[tail])) {
                        result = tail;
                        break;
                    }
                }
            }
        } catch (Exception e) {
            logger.error("telnet read I/O failure: {}", "", e);
        }
        return result;
    }

    /**
     * Reads the fromTerminal stream until the next prompt described in the
     * parameter Returns the string obtained since starting the call till the
     * next prompt prompt is the prompt to expect expectNewLine is true if a
     * carriage return is expected
     * @throws SimoConnectionException 
    
     **/


    private String tillNextPrompt(String prompt, boolean expectNewLine, String cmdStr) throws SimoConnectionException {
        String result = null;

        try {
            char lastChar = prompt.charAt(prompt.length() - 1);
            StringBuilder sb = new StringBuilder();

            char ch;
            while (result == null) {
                byte num = (byte) fromTerminal.read();
                if (isSpecialCharset) {
                    ch = new String(new byte[] { num }, charsetName.equals("\"\"")?UTF_8:charsetName).charAt(0);
                } else {
                    ch = (char) num;
                }
                ch = (char) num;
                sb.append(ch);
                if (ch == lastChar) {
                    if (sb.toString().endsWith(prompt)) {
                        int ind = sb.toString().lastIndexOf(CRString);
                        if (expectNewLine && (ind != -1)) {
                            // verified that indeed the whole thing ends with
                            // prompt
                            if (sb.lastIndexOf(prompt) - CRString.length() != ind
                                    && sb.charAt(sb.lastIndexOf(prompt) - 1) != ']') {
                                continue;
                            }
                            result = sb.toString().substring(0, ind).trim();

                            if (result.startsWith(cmdStr)) {
                                if (result.startsWith(cmdStr + "\r\n")) {
                                    result = result.substring(cmdStr.length() + 2);
                                } else {
                                    result = result.substring(cmdStr.length());
                                }
                            }

                        } else if (expectNewLine && ind == -1 && sb.toString().trim().length() == prompt.length()) {
                            result = "";
                        } else if (!expectNewLine) {
                            // does not expect new line and there is indeed no
                            // new line
                            result = "";
                        }
                    }
                }
            }
        } catch (Exception e) {
            logger.error("telnet read I/O failure: {}", "", e);
            throw new SimoConnectionException(e);
        }
        return result;
    }

    @Override
    public boolean isConnected() {
        connected = telnet != null && telnet.isAvailable();
        return connected;
    }
}
